
import KaseoDeviceInterface from "../KaseoDeviceInterface";

export const KASEO_DEV_CMONITOR_DI = Symbol()


export interface Command2D {
    execute: (ctx: CanvasRenderingContext2D, gdi: KaseoCommon2dGDI) => void
    onPush?: (gdi: KaseoCommon2dGDI) => void
}


class _Cmd2dClear implements Command2D {
    execute = (ctx: CanvasRenderingContext2D, gdi: KaseoCommon2dGDI) => {
        ctx.clearRect(0, 0, gdi.getScreenWidth(), gdi.getScreenHeight());
    }
}
export const CMD_2D_CLEAR = new _Cmd2dClear

class _Cmd2dPushState implements Command2D {
    execute = (ctx: CanvasRenderingContext2D, gdi: KaseoCommon2dGDI) => ctx.save()
}
export const CMD_2D_PUSH_STATE = new _Cmd2dPushState()

class _Cmd2dPopState implements Command2D {
    execute = (ctx: CanvasRenderingContext2D, gdi: KaseoCommon2dGDI) => ctx.restore()
}
export const CMD_2D_POP_STATE = new _Cmd2dPopState()

export interface Font2D {
    style?: FontStyle
    family: string
    size: number
}

export class Cmd2dPushFont implements Command2D {
    private style: FontStyle = 'normal'
    private family: string = 'serif'
    private size: number = 12

    onPush(gdi: KaseoCommon2dGDI) {
        this.style = gdi.getContextFontStyle()
        this.family = gdi.getContextFontFamily()
        this.size = gdi.getContextFontSize()
    }

    execute(ctx: CanvasRenderingContext2D, gdi: KaseoCommon2dGDI) {
        gdi.getContext().font = `${this.style} ${this.size}px ${this.family}`
    }
}

export class Cmd2dDrawLine implements Command2D {
    ax: number
    ay: number
    bx: number
    by: number

    constructor(ax: number, ay: number, bx: number, by: number) {
        this.ax = ax
        this.ay = ay
        this.bx = bx
        this.by = by
    }
    
    execute(ctx: CanvasRenderingContext2D, gdi: KaseoCommon2dGDI) {
        ctx.moveTo(this.ax, this.ay)
        ctx.lineTo(this.bx, this.by)
        ctx.stroke()
    }
}

export class Cmd2dSetStrokeColor implements Command2D {
    private readonly color: string

    /**
     * 
     * @param r [0..1]
     * @param g [0..1]
     * @param a [0..1]
     */
    constructor(r: number, g: number, b: number) 
    {
        if (r > 1) r = 1
        if (r < 0) r = 0
        
        if (g > 1) g = 1
        if (g < 0) g = 0
        
        if (b > 1) b = 1
        if (b < 0) b = 0
        
        const rhex = Math.floor(r * 255).toString(16).padEnd(2, '0')
        const ghex = Math.floor(g * 255).toString(16).padEnd(2, '0')
        const bhex = Math.floor(b * 255).toString(16).padEnd(2, '0')
        
        this.color = `#${rhex}${ghex}${bhex}`
    }
    
    execute(ctx: CanvasRenderingContext2D, gdi: KaseoCommon2dGDI) {
        ctx.strokeStyle = this.color
    }
}

export class Cmd2dSetFillColor implements Command2D {
    private readonly color: string

    /**
     * 
     * @param r [0..1]
     * @param g [0..1]
     * @param a [0..1]
     */
    constructor(r: number, g: number, b: number) 
    {
        if (r > 1) r = 1
        if (r < 0) r = 0
        
        if (g > 1) g = 1
        if (g < 0) g = 0
        
        if (b > 1) b = 1
        if (b < 0) b = 0
        
        const rhex = Math.floor(r * 255).toString(16).padEnd(2, '0')
        const ghex = Math.floor(g * 255).toString(16).padEnd(2, '0')
        const bhex = Math.floor(b * 255).toString(16).padEnd(2, '0')
        
        this.color = `#${rhex}${ghex}${bhex}`
    }
    
    execute(ctx: CanvasRenderingContext2D, gdi: KaseoCommon2dGDI) {
        ctx.fillStyle = this.color
    }
}

export class Cmd2dFillRect implements Command2D {
    x: number
    y: number
    w: number
    h: number

    constructor(x: number, y: number, w: number, h: number) {
        this.x = x
        this.y = y
        this.w = w
        this.h = h
    }

    execute(ctx: CanvasRenderingContext2D, gdi: KaseoCommon2dGDI) {
        ctx.fillRect(this.x, this.y, this.w, this.h)
    }
}

export class Cmd2dDrawText implements Command2D {
    text: string
    x: number
    y: number
    stroke: boolean
    maxWidth: number | undefined

    constructor(text: string, x: number, y: number, 
                
                stroke: boolean = false, maxWidth?: number) {
        this.text = text
        this.x = x
        this.y = y
        this.stroke = stroke
        this.maxWidth = maxWidth
    }

    execute(ctx: CanvasRenderingContext2D, gdi: KaseoCommon2dGDI) {
        this.stroke ? 
            ctx.strokeText(this.text, this.x, this.y, this.maxWidth) : 
            ctx.fillText(this.text, this.x, this.y, this.maxWidth)
    }
}


export class Cmd2dTranslate implements Command2D {
    x: number
    y: number

    constructor(x: number, y: number) {
        this.x = x
        this.y = y
    }

    execute(ctx: CanvasRenderingContext2D, gdi: KaseoCommon2dGDI) {
        ctx.translate(this.x, this.y)
    }
}


export class Cmd2dClipRect implements Command2D {
    x: number
    y: number
    w: number
    h: number

    constructor(x: number, y: number, w: number, h: number) {
        this.x = x
        this.y = y
        this.w = w
        this.h = h
    }

    execute(ctx: CanvasRenderingContext2D, gdi: KaseoCommon2dGDI) {
        ctx.rect(this.x, this.y, this.w, this.h)
        ctx.clip()
    }
}


export interface KaseoCommonGDI {
    flushDrawCommand: () => void

    getScreenHeight: () => number
    getScreenWidth: () => number
}


export interface KaseoCommon2dGDI extends KaseoCommonGDI {
    pushCommand2d: (command: Command2D) => void
    pushAllCommand2d: (commands: Command2D[]) => void

    setContextFontFamily: (fontFamily: string) => void
    setContextFontSize: (fontSize: number) => void
    setContextFontStyle: (fontStyle: FontStyle) => void
    getContextFontFamily: () => string
    getContextFontSize: () => number
    getContextFontStyle: () => FontStyle

    measureText: (text: string) => TextMetrics

    getContext: () => CanvasRenderingContext2D
}


type FontStyle = 
    | 'normal'
    | 'bold'
    | 'bolder'
    | 'lighter'
    | '100'
    | '200'
    | '300'
    | '400'
    | '500'
    | '600'
    | '700'
    | '800'
    | '900'
    | '1000'


export default class KaseoCanvas2dGraphicsDI extends KaseoDeviceInterface implements KaseoCommon2dGDI {
    private ctx: CanvasRenderingContext2D
    private bufPtr: number = 0

    public readonly width: number
    public readonly height: number 

    private ctxFontFamily: string = 'serif'
    private ctxFontSize: number = 12
    private ctxFontStyle: FontStyle = 'normal'

    constructor(ctx: CanvasRenderingContext2D, width: number, height: number, devName: string) {
        super(KASEO_DEV_CMONITOR_DI, devName)
        this.ctx = ctx

        this.width = width
        this.height = height

        this.updateContextFont()
    }

    private getDrawCommandBuffer() {
        return this.readMem(0) as Array<Command2D>
    }

    private executeCommandBuffer(cmdBuf: Array<Command2D>) {
        for (let i = 0; i < this.bufPtr; i++) {
            cmdBuf[i].execute(this.ctx, this)
            
        }
    }

    $$devSetup(): void {
        this.writeMem(0, new Array<Command2D>(128))
    }

    $$devTickBefore(): void {
        this.flushDrawCommand()
    }

    $$devTickAfter(): void {
        this.flushDrawCommand()
    }

    pushCommand2d(command: Command2D) {
        const cmdBuf = this.getDrawCommandBuffer()

        if (cmdBuf === undefined) {
            console.warn('Kaseo-Canvas2dGDI: cannot push DrawCommand2d')
            return this
        }

        if (command.onPush !== undefined) 
            command.onPush(this)

        cmdBuf[this.bufPtr] = command
        this.bufPtr += 1

        return this
    }

    pushAllCommand2d(commands: Command2D[]) {
        for (let i = 0; i < commands.length; ++i)
            this.pushCommand2d(commands[i])
    }

    flushDrawCommand() {
        let cmdBuf: Array<Command2D> | undefined = this.getDrawCommandBuffer()
        if (!cmdBuf)
            return

        this.executeCommandBuffer(cmdBuf)

        this.bufPtr = 0
    }

    getScreenHeight = () => this.height
    getScreenWidth = () => this.width

    private updateContextFont(fontStr?: string) {
        if (fontStr === undefined)
            this.ctx.font = `${this.ctxFontStyle} ${this.ctxFontSize}px ${this.ctxFontFamily}`
        else
            this.ctx.font = fontStr
    }

    setContextFontSize(fontSize: number) {
        this.ctxFontSize = fontSize

        this.updateContextFont()
    }

    setContextFontFamily(fontFamily: string) {
        this.ctxFontFamily = fontFamily

        this.updateContextFont()
    }

    setContextFontStyle(fontStyle: FontStyle) {
        this.ctxFontStyle = fontStyle

        this.updateContextFont()
    }

    getContextFontFamily(): string {
        return this.ctxFontFamily
    }

    getContextFontSize(): number {
        return this.ctxFontSize
    }

    getContextFontStyle(): FontStyle {
        return this.ctxFontStyle
    }

    measureText = (text: string) => this.ctx.measureText(text);

    getContext(): CanvasRenderingContext2D {
        return this.ctx
    }
}
